/**
 * gnome-gmail-notifier: the gnome gmail notifier.
 * Copyright (C) 2007 Bradley A. Worley.
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License,
 * or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the
 * 
 *     Free Software Foundation, Inc.
 *     59 Temple Place, Suite 330
 *     Boston, MA 02111-1307 USA
 **/

/*
 * include our application header.
 */
#include <main.h>

/*
 * private object definition.
 */
struct _GgnXmlParserPrivate {
    /* the xml parser. */
    xmlTextReader* reader;
    
    /* current node information. */
    gint type;
    gchar* name;
    gchar* text;
    
    /* a non-conforming XPath string */
    gchar* xpath;
    
    /* attribute information. */
    gint n_attr;
    GHashTable* attribs;
};

/*
 * forward function definitions.
 */
static void ggn_xml_parser_init (GgnXmlParser* self);
static void ggn_xml_parser_class_init (GgnXmlParserClass* klass);
static void ggn_xml_parser_finalize (GObject* obj);

/*
 * define the gobject type and its basic functions.
 */
G_DEFINE_TYPE (GgnXmlParser, ggn_xml_parser, G_TYPE_OBJECT);

/*
 * define the xml node types.
 */
#define GGN_XML_NODE_TYPE_TEXT          3
#define GGN_XML_NODE_TYPE_OPENTAG       1
#define GGN_XML_NODE_TYPE_CLOSETAG      15

/*
 * define the signals used.
 */
enum {
    BEGIN_ELEMENT,
    END_ELEMENT,
    TEXT,
    LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = { 0 };

/*
 * ggn_xml_parser_default_begin_element_cb:
 *
 * This is the default "begin_element" callback function manager.
 *
 * Return value: void.
 */
static void ggn_xml_parser_default_begin_element_cb (GgnXmlParser* parser,
                                                     gchar* element_path,
                                                     gchar* element_name,
                                                     GHashTable* attributes) {
    /* do nothing. */
}

/*
 * ggn_xml_parser_default_end_element_cb:
 *
 * This is the default "end_element" callback function manager.
 *
 * Return value: void.
 */
static void ggn_xml_parser_default_end_element_cb (GgnXmlParser* parser,
                                                   gchar* element_path,
                                                   gchar* element_name) {
    /* do nothing. */
}

/*
 * ggn_xml_parser_default_text_cb:
 *
 * This is the default "text" callback function manager.
 *
 * Return value: void.
 */
static void ggn_xml_parser_default_text_cb (GgnXmlParser* parser,
                                            gchar* element_path,
                                            gchar* element_name,
                                            gchar* value) {
    /* do nothing. */
}

/*
 * ggn_xml_parser_init:
 *
 * This function is used by the gobject library to
 * generate a new instance of our object.
 */
static void ggn_xml_parser_init (GgnXmlParser* self) {
    /* set up the private data structure. */
    self->priv = g_new0 (GgnXmlParserPrivate, 1);
    
    /* setup private instance objects. */
    self->priv->reader = NULL;
}

/*
 * ggn_xml_parser_class_init:
 *
 * This function is used by the gobject library to
 * generate a new class object of our object.
 */
static void ggn_xml_parser_class_init (GgnXmlParserClass* klass) {
    /* setup a gobject class. */
    GObjectClass* gobj_class = G_OBJECT_CLASS (klass);
    
    /* set the locations of our destruction function. */
    gobj_class->finalize = ggn_xml_parser_finalize;
    
    /* setup the default signal handler. */
    klass->begin_element = ggn_xml_parser_default_begin_element_cb;
    klass->end_element = ggn_xml_parser_default_end_element_cb;
    klass->text = ggn_xml_parser_default_text_cb;
    
    /*
     * GgnXmlParser::begin_element:
     *
     * Emitted when the parser encounters an opened XML tag.
     */
    signals[BEGIN_ELEMENT] = g_signal_new ("begin_element",
                                           G_OBJECT_CLASS_TYPE (gobj_class),
                                           G_SIGNAL_RUN_FIRST,
                                           G_STRUCT_OFFSET (GgnXmlParserClass, begin_element),
                                           NULL, NULL,
                                           ggn_marshal_VOID__STRING_STRING_OBJECT,
                                           G_TYPE_NONE, 3,
                                           G_TYPE_STRING,
                                           G_TYPE_STRING,
                                           G_TYPE_HASH_TABLE);
    
    /*
     * GgnXmlParser::end_element:
     *
     * Emitted when the parser encounters a closed XML tag.
     */
    signals[END_ELEMENT] = g_signal_new ("end_element",
                                         G_OBJECT_CLASS_TYPE (gobj_class),
                                         G_SIGNAL_RUN_FIRST,
                                         G_STRUCT_OFFSET (GgnXmlParserClass, begin_element),
                                         NULL, NULL,
                                         ggn_marshal_VOID__STRING_STRING,
                                         G_TYPE_NONE, 2,
                                         G_TYPE_STRING,
                                         G_TYPE_STRING);
    
    /*
     * GgnXmlParser::text:
     *
     * Emitted when the parser encounters text content.
     */
    signals[TEXT] = g_signal_new ("text",
                                  G_OBJECT_CLASS_TYPE (gobj_class),
                                  G_SIGNAL_RUN_FIRST,
                                  G_STRUCT_OFFSET (GgnXmlParserClass, begin_element),
                                  NULL, NULL,
                                  ggn_marshal_VOID__STRING_STRING_STRING,
                                  G_TYPE_NONE, 3,
                                  G_TYPE_STRING,
                                  G_TYPE_STRING,
                                  G_TYPE_STRING);
}

/*
 * ggn_xml_parser_finalize:
 *
 * This function is used by the gobject library to cleanly finish
 * the destruction process started by the dispose function.
 */
static void ggn_xml_parser_finalize (GObject* obj) {
    /* make a reference to ourself. */
    GgnXmlParser* self = GGN_XML_PARSER (obj);
    
    /* free the private instance objects. */
    
    /* destroy the private object. */
    g_free (self->priv);
    self->priv = NULL;
    
    /* chain up to the parent class. */
    G_OBJECT_CLASS (ggn_xml_parser_parent_class)->finalize (obj);
}

/*
 * ggn_xml_parser_new:
 *
 * Creates a new GgnXmlParser with default values, which are
 * used to parse any XML-based strings.
 *
 * Return value: the new xml parser.
 */
GgnXmlParser* ggn_xml_parser_new (void) {
    /* make a newly created gobject. */
    GgnXmlParser* parser = g_object_new (GGN_TYPE_XML_PARSER, NULL);
    
    /* return the new object. */
    return parser;
}

/*
 * ggn_xml_parser_free:
 *
 * Frees the given xml parser by decreasing its reference count.
 *
 * Return value: void.
 */
void ggn_xml_parser_free (GgnXmlParser* parser) {
    /* unreference the object. */
    while (G_IS_OBJECT (parser)) {
        /* unreference this object. */
        g_object_unref (G_OBJECT (parser));
    }
}

/*
 * xmlTextReaderXPath:
 *
 * This function builds a totally non-conforming XPath expression from
 * an xmlTextReader object by extracting the current xmlNode and then
 * working backwards to the document node.
 *
 * Return value: the XPath string.
 */
gchar* xmlTextReaderXPath (xmlTextReader* reader) {
    /* make a GString. */
    GString* gstr = g_string_new ("");
    gchar* rev;
    
    /* get the node. */
    xmlNode* node = xmlTextReaderCurrentNode (reader);
    rev = g_utf8_strreverse ((gchar*) node->name, -1);
    g_string_append_printf (gstr, "%s/", rev);
    g_free (rev);
    
    /* loop while there are still parents. */
    node = node->parent;
    while (node->name != NULL) {
        /* append the string. */
        rev = g_utf8_strreverse ((gchar*) node->name, -1);
        g_string_append_printf (gstr, "%s/", rev);
        g_free (rev);
    
        /* get the parent node. */
        node = node->parent;
    }
    
    /* create a new string from our GString. */
    gchar* str = g_utf8_strreverse (gstr->str, -1);
    g_string_free (gstr, FALSE);
    
    /* return the new string. */
    return str;
}

/*
 * ggn_xml_parser_load_string:
 *
 * Loads an XML subset from a string in memory, as represented by
 * a gchar* variable, and then begins to parse the XML.
 *
 * Return value: success boolean.
 */
gboolean ggn_xml_parser_load_string (GgnXmlParser* parser, gchar* xml_str) {
    /* define some variables for parsing. */
    parser->priv->reader = xmlReaderForDoc ((xmlChar*) xml_str,
                                            NULL,
                                            GGN_UTIL_ENCODING,
                                            XML_PARSE_RECOVER | XML_PARSE_NOERROR);
    
    /* create the attributes hash table. */
    parser->priv->attribs = g_hash_table_new (g_str_hash, g_str_equal);
    
    /* loop and continue parsing. */
    while (xmlTextReaderRead (parser->priv->reader) == 1) {
        /* determine the xml node type. */
        parser->priv->type = xmlTextReaderNodeType (parser->priv->reader);
        
        /* act variably by the node type. */
        if (parser->priv->type == GGN_XML_NODE_TYPE_OPENTAG) {
            /* get the number of attributes. */
            parser->priv->n_attr = xmlTextReaderAttributeCount (parser->priv->reader);
            
            /* get the node name. */
            parser->priv->name = (gchar*) xmlTextReaderConstName (parser->priv->reader);
            
            /* get a non-conforming xpath string. */
            parser->priv->xpath = xmlTextReaderXPath (parser->priv->reader);
            
            /* reset the hash table. */
            g_hash_table_remove_all (parser->priv->attribs);
            
            /* do we have attributes? */
            if (parser->priv->n_attr > 0) {
                /* read the attributes into the hash table. */
                while (xmlTextReaderMoveToNextAttribute (parser->priv->reader)) {
                    /* add the strings to our hash table. */
                    g_hash_table_insert (parser->priv->attribs,
                        (gchar*) xmlTextReaderConstName (parser->priv->reader),
                        (gchar*) xmlTextReaderConstValue (parser->priv->reader));
                }
            }
            
            /* emit the "begin_element" signal. */
            g_signal_emit (parser, signals[BEGIN_ELEMENT], 0,
                           parser->priv->xpath,
                           parser->priv->name,
                           parser->priv->attribs);
            
            /* reset our integer values. */
            parser->priv->n_attr = 0;
        }
        else if (parser->priv->type == GGN_XML_NODE_TYPE_CLOSETAG) {
            /* get the node name. */
            parser->priv->name = (gchar*) xmlTextReaderConstName (parser->priv->reader);
            
            /* get a non-conforming xpath string. */
            parser->priv->xpath = xmlTextReaderXPath (parser->priv->reader);
            
            /* emit the "end_element" signal. */
            g_signal_emit (parser, signals[END_ELEMENT], 0,
                           parser->priv->xpath,
                           parser->priv->name);
            
            /* free the strings we allocated. */
            g_free (parser->priv->xpath);
        }
        else if (parser->priv->type == GGN_XML_NODE_TYPE_TEXT) {
            /* get the node name. */
            parser->priv->name = (gchar*) xmlTextReaderConstName (parser->priv->reader);
            
            /* get a non-conforming xpath string. */
            parser->priv->xpath = xmlTextReaderXPath (parser->priv->reader);
            
            /* get the content text. */
            parser->priv->text = (gchar*) xmlTextReaderReadString (parser->priv->reader);
            
            /* emit the "text" signal. */
            g_signal_emit (parser, signals[TEXT], 0,
                           parser->priv->xpath,
                           parser->priv->name,
                           parser->priv->text);
            
            /* free the strings we allocated. */
            g_free (parser->priv->xpath);
            g_free (parser->priv->text);
        }
        
        /* reset our integer values. */
        parser->priv->type = 0;
    }
    
    /* free the attributes hash table. */
    g_hash_table_destroy (parser->priv->attribs);
    
    /* free the reader. */
    xmlFreeTextReader (parser->priv->reader);

    /* set the exit status. */
    return TRUE;
}

/*
 * ggn_xml_parser_load_file:
 *
 * Loads an XML subset from an XML file, as represented by
 * a gchar* variable, and then begins to parse the XML.
 *
 * Return value: success boolean.
 */
gboolean ggn_xml_parser_load_file (GgnXmlParser* parser, gchar* xml_file) {
    /* make sure the file exists. */
    if (!g_file_test (xml_file, G_FILE_TEST_IS_REGULAR)) {
        /* exit the function. */
        return FALSE;
    }
    
    /* declare a string for the file contents. */
    gchar* body = NULL;
    
    /* read in the file's contents.. */
    if (!g_file_get_contents (xml_file, &body, NULL, NULL)) {
        /* exit the function. */
        return FALSE;
    }
    
    /* run the string parsing function. */
    gboolean status = ggn_xml_parser_load_string (parser, body);
    
    /* free the string. */
    g_free (body);
    
    /* set the exit status. */
    return status;
}
